/*
 * Copyright (c) 2012-2020 MIRACL UK Ltd.
 *
 * This file is part of MIRACL Core
 * (see https://github.com/miracl/core).
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/*
 * Implementation of the ctx.AES-GCM Encryption/Authentication
 *
 * Some restrictions..
 * 1. Only for use with ctx.AES
 * 2. Returned tag is always 128-bits. Truncate at your own risk.
 * 3. The order of function calls must follow some rules
 *
 * Typical sequence of calls..
 * 1. call GCM_init
 * 2. call GCM_add_header any number of times, as long as length of header is multiple of 16 bytes (block size)
 * 3. call GCM_add_header one last time with any length of header
 * 4. call GCM_add_cipher any number of times, as long as length of cipher/plaintext is multiple of 16 bytes
 * 5. call GCM_add_cipher one last time with any length of cipher/plaintext
 * 6. call GCM_finish to extract the tag.
 *
 * See http://www.mindspring.com/~dmcgrew/gcm-nist-6.pdf
 */

var GCM = function(ctx) {
    "use strict";

    var GCM = function() {
        this.table = new Array(128);
        for (var i = 0; i < 128; i++) {
            this.table[i] = new Array(4); /* 2k bytes */
        }
        this.stateX = [];
        this.Y_0 = [];
        this.counter = 0;
        this.lenA = [];
        this.lenC = [];
        this.status = 0;
        this.a = new ctx.AES();
    };

    // GCM constants

    GCM.ACCEPTING_HEADER = 0;
    GCM.ACCEPTING_CIPHER = 1;
    GCM.NOT_ACCEPTING_MORE = 2;
    GCM.FINISHED = 3;
    GCM.ENCRYPTING = 0;
    GCM.DECRYPTING = 1;

    GCM.prototype = {
        precompute: function(H) {
            var b = [],
                i, j, c;

            for (i = j = 0; i < 4; i++, j += 4) {
                b[0] = H[j];
                b[1] = H[j + 1];
                b[2] = H[j + 2];
                b[3] = H[j + 3];
                this.table[0][i] = GCM.pack(b);
            }
            for (i = 1; i < 128; i++) {
                c = 0;
                for (j = 0; j < 4; j++) {
                    this.table[i][j] = c | (this.table[i - 1][j]) >>> 1;
                    c = this.table[i - 1][j] << 31;
                }

                if (c !== 0) {
                    this.table[i][0] ^= 0xE1000000; /* irreducible polynomial */
                }
            }
        },

        gf2mul: function() { /* gf2m mul - Z=H*X mod 2^128 */
            var P = [],
                b = [],
                i, j, m, k, c;

            P[0] = P[1] = P[2] = P[3] = 0;
            j = 8;
            m = 0;

            for (i = 0; i < 128; i++) {
                c = (this.stateX[m] >>> (--j)) & 1;
                c = ~c + 1;
                for (k = 0; k < 4; k++) {
                    P[k] ^= (this.table[i][k] & c);
                }

                if (j === 0) {
                    j = 8;
                    m++;
                    if (m == 16) {
                        break;
                    }
                }
            }

            for (i = j = 0; i < 4; i++, j += 4) {
                b = GCM.unpack(P[i]);
                this.stateX[j] = b[0];
                this.stateX[j + 1] = b[1];
                this.stateX[j + 2] = b[2];
                this.stateX[j + 3] = b[3];
            }
        },

        wrap: function() { /* Finish off GHASH */
            var F = [],
                L = [],
                b = [],
                i, j;

            /* convert lengths from bytes to bits */
            F[0] = (this.lenA[0] << 3) | (this.lenA[1] & 0xE0000000) >>> 29;
            F[1] = this.lenA[1] << 3;
            F[2] = (this.lenC[0] << 3) | (this.lenC[1] & 0xE0000000) >>> 29;
            F[3] = this.lenC[1] << 3;

            for (i = j = 0; i < 4; i++, j += 4) {
                b = GCM.unpack(F[i]);
                L[j] = b[0];
                L[j + 1] = b[1];
                L[j + 2] = b[2];
                L[j + 3] = b[3];
            }

            for (i = 0; i < 16; i++) {
                this.stateX[i] ^= L[i];
            }

            this.gf2mul();
        },

        /* Initialize GCM mode */
        init: function(nk, key, niv, iv) { /* iv size niv is usually 12 bytes (96 bits). ctx.AES key size nk can be 16,24 or 32 bytes */
            var H = [],
                b = [],
                i;

            for (i = 0; i < 16; i++) {
                H[i] = 0;
                this.stateX[i] = 0;
            }

            this.a.init(ctx.AES.ECB, nk, key, iv);
            this.a.ecb_encrypt(H); /* E(K,0) */
            this.precompute(H);

            this.lenA[0] = this.lenC[0] = this.lenA[1] = this.lenC[1] = 0;

            if (niv == 12) {
                for (i = 0; i < 12; i++) {
                    this.a.f[i] = iv[i];
                }

                b = GCM.unpack(1);
                this.a.f[12] = b[0];
                this.a.f[13] = b[1];
                this.a.f[14] = b[2];
                this.a.f[15] = b[3]; /* initialise IV */

                for (i = 0; i < 16; i++) {
                    this.Y_0[i] = this.a.f[i];
                }
            } else {
                this.status = GCM.ACCEPTING_CIPHER;
                this.ghash(iv, niv); /* GHASH(H,0,IV) */
                this.wrap();

                for (i = 0; i < 16; i++) {
                    this.a.f[i] = this.stateX[i];
                    this.Y_0[i] = this.a.f[i];
                    this.stateX[i] = 0;
                }

                this.lenA[0] = this.lenC[0] = this.lenA[1] = this.lenC[1] = 0;
            }

            this.status = GCM.ACCEPTING_HEADER;
        },

        /* Add Header data - included but not encrypted */
        add_header: function(header, len) { /* Add some header. Won't be encrypted, but will be authenticated. len is length of header */
            var i, j = 0;

            if (this.status != GCM.ACCEPTING_HEADER) {
                return false;
            }

            while (j < len) {
                for (i = 0; i < 16 && j < len; i++) {
                    this.stateX[i] ^= header[j++];
                    this.lenA[1]++;
                    this.lenA[1] |= 0;

                    if (this.lenA[1] === 0) {
                        this.lenA[0]++;
                    }
                }

                this.gf2mul();
            }

            if (len % 16 !== 0) {
                this.status = GCM.ACCEPTING_CIPHER;
            }

            return true;
        },

        ghash: function(plain, len) {
            var i, j = 0;

            if (this.status == GCM.ACCEPTING_HEADER) {
                this.status = GCM.ACCEPTING_CIPHER;
            }

            if (this.status != GCM.ACCEPTING_CIPHER) {
                return false;
            }

            while (j < len) {
                for (i = 0; i < 16 && j < len; i++) {
                    this.stateX[i] ^= plain[j++];
                    this.lenC[1]++;
                    this.lenC[1] |= 0;

                    if (this.lenC[1] === 0) {
                        this.lenC[0]++;
                    }
                }
                this.gf2mul();
            }

            if (len % 16 !== 0) {
                this.status = GCM.NOT_ACCEPTING_MORE;
            }

            return true;
        },

        /* Add Plaintext - included and encrypted */
        add_plain: function(plain, len) {
            var B = [],
                b = [],
                cipher = [],
                i, j = 0;

            if (this.status == GCM.ACCEPTING_HEADER) {
                this.status = GCM.ACCEPTING_CIPHER;
            }

            if (this.status != GCM.ACCEPTING_CIPHER) {
                return cipher;
            }

            while (j < len) {
                b[0] = this.a.f[12];
                b[1] = this.a.f[13];
                b[2] = this.a.f[14];
                b[3] = this.a.f[15];
                this.counter = GCM.pack(b);
                this.counter++;
                b = GCM.unpack(this.counter);
                this.a.f[12] = b[0];
                this.a.f[13] = b[1];
                this.a.f[14] = b[2];
                this.a.f[15] = b[3]; /* increment counter */

                for (i = 0; i < 16; i++) {
                    B[i] = this.a.f[i];
                }

                this.a.ecb_encrypt(B); /* encrypt it  */

                for (i = 0; i < 16 && j < len; i++) {
                    cipher[j] = (plain[j] ^ B[i]);
                    this.stateX[i] ^= cipher[j++];
                    this.lenC[1]++;
                    this.lenC[1] |= 0;

                    if (this.lenC[1] === 0) {
                        this.lenC[0]++;
                    }
                }

                this.gf2mul();
            }

            if (len % 16 !== 0) {
                this.status = GCM.NOT_ACCEPTING_MORE;
            }

            return cipher;
        },

        /* Add Ciphertext - decrypts to plaintext */
        add_cipher: function(cipher, len) {
            var B = [],
                b = [],
                plain = [],
                j = 0,
                i, oc;

            if (this.status == GCM.ACCEPTING_HEADER) {
                this.status = GCM.ACCEPTING_CIPHER;
            }

            if (this.status != GCM.ACCEPTING_CIPHER) {
                return plain;
            }

            while (j < len) {
                b[0] = this.a.f[12];
                b[1] = this.a.f[13];
                b[2] = this.a.f[14];
                b[3] = this.a.f[15];
                this.counter = GCM.pack(b);
                this.counter++;
                b = GCM.unpack(this.counter);
                this.a.f[12] = b[0];
                this.a.f[13] = b[1];
                this.a.f[14] = b[2];
                this.a.f[15] = b[3]; /* increment counter */

                for (i = 0; i < 16; i++) {
                    B[i] = this.a.f[i];
                }

                this.a.ecb_encrypt(B); /* encrypt it  */

                for (i = 0; i < 16 && j < len; i++) {
                    oc = cipher[j];
                    plain[j] = (cipher[j] ^ B[i]);
                    this.stateX[i] ^= oc;
                    j++;
                    this.lenC[1]++;
                    this.lenC[1] |= 0;

                    if (this.lenC[1] === 0) {
                        this.lenC[0]++;
                    }
                }

                this.gf2mul();
            }

            if (len % 16 !== 0) {
                this.status = GCM.NOT_ACCEPTING_MORE;
            }

            return plain;
        },

        /* Finish and extract Tag */
        finish: function(extract) { /* Finish off GHASH and extract tag (MAC) */
            var tag = [],
                i;

            this.wrap();
            /* extract tag */
            if (extract) {
                this.a.ecb_encrypt(this.Y_0); /* E(K,Y0) */

                for (i = 0; i < 16; i++) {
                    this.Y_0[i] ^= this.stateX[i];
                }

                for (i = 0; i < 16; i++) {
                    tag[i] = this.Y_0[i];
                    this.Y_0[i] = this.stateX[i] = 0;
                }
            }

            this.status = GCM.FINISHED;
            this.a.end();

            return tag;
        }

    };

    GCM.pack = function(b) { /* pack 4 bytes into a 32-bit Word */
        return (((b[0]) & 0xff) << 24) | ((b[1] & 0xff) << 16) | ((b[2] & 0xff) << 8) | (b[3] & 0xff);
    };

    GCM.unpack = function(a) { /* unpack bytes from a word */
        var b = [];

        b[3] = (a & 0xff);
        b[2] = ((a >>> 8) & 0xff);
        b[1] = ((a >>> 16) & 0xff);
        b[0] = ((a >>> 24) & 0xff);

        return b;
    };

    GCM.hex2bytes = function(s) {
        var len = s.length,
            data = [],
            i;

        for (i = 0; i < len; i += 2) {
            data[i / 2] = parseInt(s.substr(i, 2), 16);
        }

        return data;
    };

    GCM.encrypt = function(C,T,K,IV,H,P) {
	    var g=new GCM();
	    g.init(K.length,K,IV.length,IV);
	    g.add_header(H,H.length);
	    var b=g.add_plain(P,P.length);
        for (var i=0;i<b.length;i++) C[i]=b[i];
	    b=g.finish(true);
        for (var i=0;i<b.length;i++) T[i]=b[i];
    };

    GCM.decrypt = function(P,T,K,IV,H,C) {
	    var g=new GCM();
	    g.init(K.length,K,IV.length,IV);
	    g.add_header(H,H.length);
	    var b=g.add_cipher(C,C.length);
        for (var i=0;i<b.length;i++) P[i]=b[i];
	    b=g.finish(true);
	    for (var i=0;i<b.length;i++) T[i]=b[i];
    };

    return GCM;
};

if (typeof module !== "undefined" && typeof module.exports !== "undefined") {
    module.exports = {
        GCM: GCM
    };
}

